Conversation
Walkthrough유저 프로필 조회 API를 연동하고, 조회된 이메일과 닉네임을 '내 서재' 화면에 표시하는 기능이 추가되었습니다. 이를 위해 데이터 모델, 네트워크 응답, 레포지토리, 프레젠터, UI 등 전반적으로 코드가 확장 및 수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as LibraryScreen
participant Presenter as LibraryPresenter
participant UserRepo as UserRepository (DefaultUserRepository)
participant AuthSvc as AuthService
UI->>Presenter: present() 호출 (초기 컴포즈)
activate Presenter
Presenter->>UserRepo: getUserProfile()
activate UserRepo
UserRepo->>AuthSvc: getUserProfile()
activate AuthSvc
AuthSvc-->>UserRepo: UserProfileResponse 반환
UserRepo-->>Presenter: Result<UserProfileModel> 반환
Presenter-->>UI: 상태 업데이트 (nickname, email)
deactivate Presenter
Assessment against linked issues
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt (1)
85-95: UI 구조 개선을 고려해보세요.사용자 프로필 정보 표시 기능이 잘 구현되어 있지만, 중첩된
Column구조가 다소 복잡합니다.다음과 같이 구조를 단순화할 수 있습니다:
- Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "내 서재") - Spacer(modifier = Modifier.height(16.dp)) - Text(text = state.nickname) - Spacer(modifier = Modifier.height(16.dp)) - Text(text = state.email) - } + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(text = "내 서재") + Spacer(modifier = Modifier.height(16.dp)) + Text(text = state.nickname) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = state.email) + }feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (1)
38-66: 유저 프로필 조회 함수가 잘 구현되었습니다.로딩 상태 관리, 에러 핸들링, 성공/실패 케이스 처리가 모두 적절히 구현되어 있습니다.
handleException유틸리티 함수를 사용하여 일관된 에러 처리를 하고 있어 좋습니다.다만 함수명을
loadUserProfile()이나fetchUserProfile()로 변경하는 것을 고려해보세요. 현재getUserProfile()은 데이터를 반환하는 것처럼 보이지만 실제로는 사이드 이펙트를 수행하는 함수입니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt(1 hunks)core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt(1 hunks)core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt(2 hunks)core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt(1 hunks)core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt(1 hunks)core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt(1 hunks)core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt(1 hunks)core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/AuthService.kt(1 hunks)feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt(4 hunks)feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt(5 hunks)gradle/libs.versions.toml(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt (1)
build-logic/src/main/kotlin/com/ninecraft/booket/convention/Dependencies.kt (1)
implementation(6-8)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (1)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt (1)
runSuspendCatching(16-30)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt (1)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt (1)
HandleLibrarySideEffects(8-28)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (1)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt (1)
handleException(9-36)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: ci-build
🔇 Additional comments (21)
gradle/libs.versions.toml (1)
18-18: compose-effects 라이브러리 버전 최신 및 보안 취약점 없음 확인
skydoves/compose-effects최신 릴리스 태그가0.1.1임을 확인했습니다.- GitHub 및 공개 보안 데이터베이스 검색 결과 해당 버전에 대한 알려진 취약점이 보고되지 않았습니다.
위 의존성 추가를 승인합니다.
build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt (1)
27-27: 의존성 추가 잘 구현됨compose-effects 라이브러리가 Android Feature Convention Plugin에 적절히 추가되었습니다. 기존 의존성 패턴과 일관성을 유지하고 있습니다.
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt (2)
4-4: 임포트 추가 적절함UserProfileModel과 UserProfileResponse 임포트가 새로운 기능에 필요한 타입들을 적절히 추가했습니다.
Also applies to: 6-6
15-22: 매퍼 함수 구현이 올바름UserProfileResponse를 UserProfileModel로 변환하는 확장 함수가 깔끔하게 구현되었습니다. 모든 필드가 적절히 매핑되어 있고, 기존 LoginResponse.toModel() 함수와 일관된 패턴을 따르고 있습니다.
core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/AuthService.kt (2)
3-4: 필요한 임포트 추가됨UserProfileResponse와 GET 어노테이션 임포트가 새로운 API 메서드에 필요한 타입들을 적절히 추가했습니다.
11-12: 인증 토큰 전송 방식 확인 완료
- TokenInterceptor.kt에서
chain.request().newBuilder().addHeader("Authorization", "Bearer $accessToken")로 헤더를 자동 추가- NetworkModule.kt의 AuthOkHttpClient에 TokenInterceptor가 등록되어 AuthService 호출에 자동 적용
- TokenAuthenticator.kt에서도 리프레시 후 새 토큰으로
Authorization헤더를 갱신 처리위 구현으로
getUserProfile()요청 시 Bearer 토큰이 정상 전달되므로 추가 수정은 필요 없습니다.core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt (1)
3-8: 로깅 검색 결과 없음: UserProfileModel 관련 로깅이 발견되지 않았습니다.
rg명령어로 UserProfileModel이 로그 호출에서 사용되는지 검사했으나 결과가 없습니다.
프로젝트에서 다른 로깅 라이브러리나 확장 메서드를 사용 중인지, 혹은 커스텀 래퍼를 통해 로그를 남기고 있지는 않은지 수동으로 확인해 주세요.• UserProfileModel을 로그로 기록하는 코드가 존재하지 않음
• 다른 로깅 추상화(예: SLF4J 래퍼, AOP 기반 로깅 등) 사용 여부 확인 필요core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt (1)
1-7: 깔끔한 레포지토리 인터페이스 구현입니다.리포지토리 패턴을 올바르게 구현했으며,
Result<T>와suspend함수를 사용하여 비동기 에러 핸들링을 적절히 지원하고 있습니다.core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt (1)
6-16: 직렬화 설정이 올바르게 구성된 응답 데이터 클래스입니다.
@Serializable과@SerialName어노테이션을 적절히 사용하여 JSON 직렬화를 지원하고 있습니다.모든 필드가 non-null String으로 정의되어 있는데, 실제 API 스펙에서 일부 필드(예: provider)가 nullable할 가능성이 있는지 확인해보세요.
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt (2)
4-6: 적절한 의존성 주입 설정입니다.새로운 import가 올바르게 추가되었습니다.
21-23: 일관성 있는 바인딩 설정입니다.기존
AuthRepository바인딩과 동일한 패턴을 따라@Binds와@Singleton어노테이션을 적절히 사용하고 있습니다.core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (1)
9-15: 깔끔한 레포지토리 구현입니다.의존성 주입, 에러 핸들링, 데이터 변환이 적절히 구현되어 있습니다.
runSuspendCatching을 사용하여 코루틴 안전한 에러 핸들링을 제공하고 있습니다.feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt (3)
35-36: 적절한 상태 확장입니다.사용자 프로필 정보를 표시하기 위한 필드가 적절히 추가되었으며, 기본값 설정도 올바릅니다.
57-61: 사이드 이펙트 처리 위치가 개선되었습니다.
HandleLibrarySideEffects를 최상단으로 이동하여 관심사 분리가 더 명확해졌습니다.
131-132: 프리뷰 데이터가 적절히 추가되었습니다.개발 및 디자인 검토를 위한 샘플 데이터가 적절히 제공되고 있습니다.
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (6)
10-10: 새로운 의존성 import가 적절히 추가되었습니다.유저 프로필 조회 기능 구현을 위한
UserRepository와 성능 최적화를 위한RememberedEffectimport가 올바르게 추가되었습니다.Also applies to: 13-13
26-27: 의존성 주입이 적절히 개선되었습니다.기존
AuthRepository의 네이밍을authRepository로 명확히 하고, 새로운UserRepository의존성을 추가하여 관심사를 적절히 분리했습니다.
35-36: 상태 변수가 적절히 추가되었습니다.
rememberRetained를 사용하여nickname과
68-70:RememberedEffect사용이 적절합니다.PR 목표에서 언급한 대로
LaunchedEffect대신RememberedEffect를 사용하여 성능 최적화를 달성했습니다. 컴포저블이 처음 구성될 때 한 번만 유저 프로필을 로드하는 것이 올바른 접근입니다.
82-82: 변수명 변경이 일관되게 적용되었습니다.기존 로그아웃 로직에서
repository를authRepository로 변경하여 새로운 네이밍 규칙과 일관성을 유지했습니다.Also applies to: 84-84
112-113: 반환 상태에 새로운 프로필 정보가 적절히 추가되었습니다.
nickname과
|
produceRetainedState를 사용한다면...예시
class LibraryPresenter @AssistedInject constructor(
@Assisted private val navigator: Navigator,
private val authRepository: AuthRepository,
private val userRepository: UserRepository,
) : Presenter<LibraryScreen.State> {
@Composable
override fun present(): LibraryScreen.State {
val scope = rememberCoroutineScope()
var sideEffect by rememberRetained { mutableStateOf<LibraryScreen.SideEffect?>(null) }
val userProfileState by produceRetainedState<LibraryScreen.UserProfileState>(
initialValue = LibraryScreen.UserProfileState.Loading
) {
userRepository.getUserProfile()
.onSuccess { user ->
value = LibraryScreen.UserProfileState.Success(
nickname = user.nickname,
email = user.email
)
}
.onFailure { exception ->
handleException(
exception = exception,
onServerError = { message ->
value = LibraryScreen.UserProfileState.Error(message)
},
onNetworkError = { message ->
value = LibraryScreen.UserProfileState.Error(message)
},
onLoginRequired = {
navigator.resetRoot(LoginScreen)
},
)
}
}
fun handleEvent(event: LibraryScreen.Event) {
when (event) {
is LibraryScreen.Event.InitSideEffect -> {
sideEffect = null
}
is LibraryScreen.Event.OnLogoutButtonClick -> {
// ...
}
}
}
return LibraryScreen.State(
userProfile = userProfileState,
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
}
@CircuitInject(LibraryScreen::class, ActivityRetainedComponent::class)
@AssistedFactory
fun interface Factory {
fun create(navigator: Navigator): LibraryPresenter
}
}
@Parcelize
data object LibraryScreen : Screen {
sealed interface UserProfileState {
data object Loading : UserProfileState
data class Success(val nickname: String, val email: String) : UserProfileState
data class Error(val message: String) : UserProfileState
}
data class State(
val userProfile: UserProfileState = UserProfileState.Loading,
val sideEffect: SideEffect? = null,
val eventSink: (Event) -> Unit,
) : CircuitUiState
sealed interface SideEffect {
data class ShowToast(val message: String) : SideEffect
}
sealed interface Event : CircuitUiEvent {
data object InitSideEffect : Event
data object OnLogoutButtonClick : Event
}
}
@CircuitInject(LibraryScreen::class, ActivityRetainedComponent::class)
@Composable
internal fun Library(
state: LibraryScreen.State,
modifier: Modifier = Modifier,
) {
HandleLibrarySideEffects(
state = state,
eventSink = state.eventSink,
)
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
LibraryContent(
state = state,
modifier = modifier,
)
}
}
@Composable
internal fun LibraryContent(
state: LibraryScreen.State,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Box(modifier = modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(text = "내 서재")
Spacer(modifier = Modifier.height(16.dp))
// 변경 부분(예시)
when (state.userProfile) {
is LibraryScreen.UserProfileState.Loading -> {
CircularProgressIndicator()
}
is LibraryScreen.UserProfileState.Success -> {
Text(text = state.userProfile.nickname)
Spacer(modifier = Modifier.height(16.dp))
Text(text = state.userProfile.email)
}
is LibraryScreen.UserProfileState.Error -> {
Text(text = "프로필 로딩 실패")
Spacer(modifier = Modifier.height(16.dp))
Text(text = state.userProfile.message)
}
}
}
BooketButton(
onClick = {
state.eventSink(LibraryScreen.Event.OnLogoutButtonClick)
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 32.dp, end = 32.dp, bottom = 32.dp)
.height(56.dp)
.align(Alignment.BottomCenter),
text = {
Text(
text = stringResource(id = R.string.logout),
fontSize = 18.sp,
style = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
lineHeight = 25.sp,
),
)
},
)
}
}
} |
|
|
| authRepository.logout() | ||
| .onSuccess { | ||
| repository.clearTokens() | ||
| authRepository.clearTokens() |
There was a problem hiding this comment.
로그인/로그아웃 관련 비즈니스 로직을 DefaultAuthRepository 내부에서 처리하는건 어떻게 생각해? 개인적으로 서버통신&로컬토큰 처리까지 캡슐화하고 Presenter에서는 logout()/login()만 호출하는게 나을 것 같아
There was a problem hiding this comment.
@seoyoon513 근데 관련해서 고민을 해봐야할게
repository 에서 토큰 제거 처리를 한다는 것은 현재 구현 로직상 api 성공/실패 여부와 상관없이 로그아웃을 진행한다는 의미라, 로그아웃 기능 정책이 달라지는 거거든
- 로그아웃 API 호출 -> 성공(서버에서 해당 토큰들을 블랙 리스트 처리) -> 클라이언트 디비내 토큰을 제거하고, 로그인 화면으로 이동 (현재의 구현 방식)
- 로그아웃 API 호출 -> 성공 실패 여부와 상관 없이, 클라이언트 디비내 토큰을 제거하고, 로그인 화면으로 이동(누나가 제안한 방법)
사실 로그아웃를 눌렀을때 API 호출이 실패(accessToken, refreshToken이 다 만료)하여 로그인 화면으로 이동하든, API 호출이 성공하여 로그인 화면으로 이동하든 결과만 놓고 보면 같은 것이라 기획적인 문제라고도 볼 수 있을 것 같은데(서버에서 토큰을 블랙리스트에 추가하는 여부만 다름), 이건 서버랑도 같이 얘기 나눠보면 좋을 것 같은 주제인것 같네
There was a problem hiding this comment.
오키 이건 이번주 팀세션 때 회원탈퇴랑 같이 얘기해보자
|
리뷰가 늦은 관계로 믿음의 approve 해놓겠습니다~ (기능상 문제는 없어보여요) |
@seoyoon513 화요일에도 언급을 하겠지만, 그밖에 |
🔗 관련 이슈
📙 작업 설명
🧪 테스트 내역 (선택)
📸 스크린샷 또는 시연 영상 (선택)
💬 추가 설명 or 리뷰 포인트 (선택)
produceState와 유사한 Circuit의produceRetainedState를 사용하여 non-Compose 상태를 Compose 상태로 변환하여 사용하는 것이 괜찮을 것 같다고 생각했슴니다.(관련해서 예시 코드 첨부하도록 하겠습니다.)rememberEffect는 skydoves님의 compose-effects에서 지원하는 API로LaunchedEffect와 매우 흡사하나, 내부에 코루틴 스코프가 존재하지않아, 매번 코루틴 스코프를 생성하는 비용을 아낄수있습니다. LaunchedEffect를 사용할때 내부에서 suspend 함수를 호출하지 않는 경우, flow를 collect하지 않는 경우등에 사용하면 좋을 것 같아 채택을 해보았습니다Summary by CodeRabbit
신규 기능
UI 개선
버그 수정
기타